Herzlich willkommen zum Thema DRL. DRL steht für Data Retrieval Language, also die Datenabfragesprache, und wir werden uns heute speziell damit beschäftigen, wie man Abfragen über mehrere Tabellen hinweg durchführt. Dies geschieht in relationalen Datenbanksystemen hauptsächlich mithilfe von Joins. Was genau ist ein Verbund oder Join in diesem Kontext? In modernen Datenbanksystemen werden Informationen in der Regel auf verschiedene Tabellen aufgeteilt. Das macht man, um Redundanzen zu vermeiden und die Daten sauber zu strukturieren. Dieser Vorgang wird als Normalisierung bezeichnet. Um die logische Zusammengehörigkeit der Daten, die nun in separaten Tabellen liegen, dennoch zu gewährleisten, baut man Beziehungen zwischen diesen Tabellen auf. Das geschieht oft über sogenannte Fremdschlüssel. Wenn das Datenbanksystem nun eine Anfrage verarbeiten muss, bei der Informationen aus mehreren dieser getrennten Tabellen benötigt werden, müssen die einzelnen Datensätze aus diesen Tabellen wieder zusammengeführt werden. Genau das ist die Aufgabe eines Joins. Durch einen SQL Join werden also mehrere Tabellen verknüpft, die in einer vordefinierten Beziehung zueinander stehen. Dadurch kann man die ursprünglichen, zusammenhängenden Informationen wiederherstellen, auch wenn sie physisch auf mehrere Tabellen verteilt sind. Der erste Join-Typ, den wir uns ansehen, ist der INNER Join. Stellen Sie sich zwei Tabellen vor, Tabelle eins und Tabelle zwei. Ein INNER Join verknüpft Zeilen aus Tabelle eins und Zeilen aus Tabelle zwei basierend auf einer Bedingung, die Sie festlegen. Nur wenn diese Bedingung für eine Kombination von Zeilen aus beiden Tabellen erfüllt ist, werden diese zusammengeführt und im Ergebnis der Abfrage ausgegeben. Die Syntax für einen INNER Join sieht typischerweise so aus: Man wählt alle Spalten aus, dann gibt man an, aus welcher Tabelle eins und welcher Tabelle zwei die Daten kommen, und formuliert dann die Join-Bedingung nach dem Schlüsselwort ON. Eine beispielhafte Bedingung könnte sein, dass die ID aus Tabelle eins gleich dem Fremdschlüssel in Tabelle zwei ist. Schauen wir uns ein konkretes Beispiel für einen INNER Join an. Wir haben zwei Tabellen: „rechnungen“ und „kreditkarten“. Die Tabelle „rechnungen“ enthält Informationen wie Rechnungsnummer, Kundennummer, Betrag und Kartennummer, falls die Rechnung per Kreditkarte bezahlt wurde. Die Tabelle „kreditkarten“ enthält Details zu den Kreditkarten, wie Kartennummer, Firma, Inhaber und Ablaufdatum. Unsere Aufgabe ist es, alle Rechnungen zu finden, die mit Kreditkarte bezahlt wurden, und zusätzlich die Daten der verwendeten Kreditkarte auszugeben. Mit einem INNER Join auf der Spalte „kartennummer“, die in beiden Tabellen vorkommt, verknüpfen wir nur diejenigen Rechnungen mit den entsprechenden Kreditkartendaten, bei denen eine passende Kartennummer in beiden Tabellen gefunden wird. Die Abfrage würde also die Rechnungsnummer, Kundennummer, Betrag und Kartennummer aus der Rechnungstabelle sowie Firma, Inhaber und Ablaufdatum aus der Kreditkartentabelle selektieren. Basierend auf dem Beispiel von zuvor sehen wir hier die potenziellen Ausgangstabellen „rechnungen“ und „kreditkarten“ mit einigen Beispieldatensätzen. Die Tabelle „rechnungen“ zeigt verschiedene Rechnungen, einige mit einer Kartennummer, andere mit dem Wert NULL in dieser Spalte, was auf Barzahlung hindeutet. Die Tabelle „kreditkarten“ listet verschiedene Kreditkarten mit ihren Details auf. Wenn wir den INNER Join vom vorherigen Beispiel auf diese Tabellen anwenden, erhalten wir als Ergebnis nur die Zeilen, bei denen eine übereinstimmende Kartennummer in beiden Tabellen gefunden wurde. Das Ergebnis zeigt dann die kombinierten Informationen für jede mit Kreditkarte bezahlte Rechnung. Beispielsweise würde eine Rechnung mit der Kartennummer 12345 mit der Kreditkarte verknüpft, die ebenfalls die Nummer 12345 hat. Rechnungen mit NULL bei der Kartennummer oder Kreditkarten, die keiner Rechnung zugeordnet sind, erscheinen nicht im Ergebnis. Bei der Betrachtung des Ergebnisses unseres INNER Join-Beispiels fällt zunächst auf, dass von ursprünglich sechs Rechnungen nun nur noch vier im Ergebnis übrig sind. Der Grund dafür ist, dass für die beiden Rechnungen, die wahrscheinlich bar bezahlt wurden und somit NULL in der Spalte „kartennummer“ haben, keine passende Kreditkarte in der Tabelle „kreditkarten“ gefunden werden konnte. Da bei einem INNER Join beide Seiten übereinstimmen müssen, um im Ergebnis zu erscheinen, werden diese Datensätze ausgeschlossen. Analog dazu ist auch die Kreditkarte mit der Nummer 12346 nicht im Ergebnis enthalten. Obwohl diese Kreditkarte in der Tabelle „kreditkarten“ existiert, wurde sie keiner der Rechnungen in unserer Beispieltabelle zugeordnet. Daraus können wir ableiten, dass bei einem INNER Join weder unbedingt alle Datensätze aus der einen Tabelle noch unbedingt alle Datensätze aus der anderen Tabelle im Ergebnis widergegeben werden. Es werden nur die Schnittmengen beider Tabellen basierend auf der Join-Bedingung angezeigt. Ein wichtiger Punkt bei der Arbeit mit Joins und insbesondere dem INNER Join-Beispiel ist die Behandlung von Spaltennamen, die in beiden Tabellen vorkommen. Im Beispiel ist das die Spalte „kartennummer“. Wenn Sie bei der Auswahl der Spalten mit SELECT \* alle Spalten aus beiden Tabellen auswählen, wird die Spalte „kartennummer“ im Ergebnis doppelt vorhanden sein – einmal aus der Rechnungstabelle und einmal aus der Kreditkartentabelle. Das Datenbanksystem fordert Sie in solchen Fällen auf, die Mehrdeutigkeit aufzulösen, indem Sie den Tabellennamen zusammen mit dem Spaltennamen angeben, zum Beispiel rechnungen.kartennummer oder kreditkarten.kartennummer. Das Ergebnis eines INNER Joins stimmt zudem mit einer nicht-normalisierten Speicherung aller Kreditkartenzahlungen überein, allerdings ohne die Redundanzen. Eine nicht-normalisierte Tabelle würde die Kreditkartendaten für jede einzelne Transaktion, die mit dieser Karte getätigt wurde, wiederholen, was zu unnötiger Datenduplizierung führen würde. Neben dem INNER Join gibt es weitere Arten von Joins. Eine davon ist der LEFT OUTER Join, oft auch einfach LEFT Join genannt. Beim LEFT OUTER Join werden alle Datensätze aus der linken Tabelle im Ergebnis enthalten sein. Zusätzlich werden die passenden Datensätze aus der rechten Tabelle hinzugefügt, basierend auf der angegebenen ON-Bedingung. Wenn zu einem Datensatz aus der linken Tabelle kein passender Datensatz in der rechten Tabelle gefunden werden kann, werden die Spalten der rechten Tabelle im Ergebnis mit NULL-Werten aufgefüllt. Die allgemeine Syntax sieht so aus, dass man SELECT \* von Tabelle eins links mit Tabelle zwei verbindet, wiederum mit einer ON-Bedingung. Betrachten wir ein Beispiel für einen LEFT OUTER Join. Wir verwenden wieder unsere Tabellen „rechnungen“ als linke Tabelle und „kreditkarten“ als rechte Tabelle. Wir suchen alle Rechnungen. Falls eine Rechnung per Kreditkarte bezahlt wurde, möchten wir zusätzlich die Daten der entsprechenden Kreditkarte sehen. Die Abfrage würde also die Rechnungsnummer, Kundennummer, Betrag und Kartennummer aus „rechnungen“ sowie Firma, Inhaber und Ablaufdatum aus „kreditkarten“ auswählen. Der Join erfolgt wieder über die Spalte „kartennummer“. Da es sich um einen LEFT Join handelt, werden alle Rechnungen aus der linken Tabelle „rechnungen“ im Ergebnis gelistet. Für die Rechnungen, bei denen keine passende Kreditkartennummer in der Tabelle „kreditkarten“ gefunden werden kann (also die Barzahlungen), werden die Spalten für Firma, Inhaber und Ablaufdatum aus der Tabelle „kreditkarten“ im Ergebnis NULL sein. Basierend auf den Beispieldaten der Tabellen „rechnungen“ und „kreditkarten“ vom Anfang und der LEFT OUTER Join-Abfrage sehen wir hier das resultierende Tabellenergebnis. Dieses Ergebnis zeigt alle Rechnungen. Für die Rechnungen mit den Nummern 98766 und 98770, die in der Ursprungstabelle „rechnungen“ einen NULL-Wert bei der Kartennummer hatten, sind die Spalten für Firma, Inhaber und Ablaufdatum aus der Tabelle „kreditkarten“ im Ergebnis ebenfalls mit NULL gefüllt. Für die Rechnungen mit übereinstimmenden Kartennummern, wie 98765, werden die entsprechenden Kreditkartendaten aus der rechten Tabelle „kreditkarten“ angezeigt. Eine Kreditkarte, die keiner Rechnung zugeordnet ist (wie die mit der Nummer 12346), erscheint bei einem LEFT Join nicht im Ergebnis, da die linke Tabelle „rechnungen“ die „führende“ Tabelle ist. Ein weiterer Join-Typ ist der RIGHT OUTER Join, auch kurz RIGHT Join genannt. Dieser verhält sich spiegelbildlich zum LEFT OUTER Join. Beim RIGHT OUTER Join werden alle Datensätze aus der rechten Tabelle im Ergebnis enthalten sein. Zusätzlich werden die passenden Datensätze aus der linken Tabelle hinzugefügt, basierend auf der angegebenen ON-Bedingung. Wenn zu einem Datensatz aus der rechten Tabelle kein passender Datensatz in der linken Tabelle gefunden werden kann, werden die Spalten der linken Tabelle im Ergebnis mit NULL-Werten aufgefüllt. Die allgemeine Syntax verwendet RIGHT Join anstelle von LEFT Join, ansonsten ist sie ähnlich aufgebaut. Man wählt alle Spalten aus, gibt die Tabellen an und formuliert die ON-Bedingung. Sehen wir uns ein Beispiel für einen RIGHT OUTER Join an, wieder mit den Tabellen „rechnungen“ und „kreditkarten“. Wir suchen alle Karteninformationen. Falls mit einer bestimmten Kreditkarte etwas bestellt wurde, sollen die entsprechenden Rechnungsinformationen beigefügt werden. Die Abfrage würde also die relevanten Spalten aus beiden Tabellen auswählen und einen RIGHT Join auf der Spalte „kartennummer“ durchführen. Da es sich um einen RIGHT Join handelt, werden alle Kreditkarten aus der rechten Tabelle „kreditkarten“ im Ergebnis gelistet. Für die Kreditkarten, zu denen keine passende Rechnung in der Tabelle „rechnungen“ gefunden werden kann, werden die Spalten aus der linken Tabelle „rechnungen“ im Ergebnis mit NULL-Werten aufgefüllt. Eine Rechnung ohne zugeordnete Kreditkarte erscheint bei einem RIGHT Join nicht im Ergebnis, da die rechte Tabelle „kreditkarten“ hier die „führende“ Tabelle ist. Kommen wir zu einem weiteren Join-Typ: dem FULL OUTER Join. Ein FULL OUTER Join ist im Prinzip eine Kombination aus einem LEFT Join und einem RIGHT Join. Das Hauptziel dabei ist, alle Datensätze aus beiden beteiligten Tabellen in das Ergebnis einzubeziehen. Das bedeutet, das Ergebnis enthält alle Datensätze aus der linken Tabelle und alle Datensätze aus der rechten Tabelle. Wenn für einen Datensatz aus der linken Tabelle kein passender Datensatz in der rechten Tabelle gefunden wird, werden die Spalten der rechten Tabelle mit NULL aufgefüllt. Umgekehrt, wenn für einen Datensatz aus der rechten Tabelle kein passender Datensatz in der linken Tabelle gefunden wird, werden die Spalten der linken Tabelle mit NULL aufgefüllt. Die allgemeine Syntax verwendet die Schlüsselwörter FULL OUTER Join oder einfach FULL Join zwischen den Tabellennamen, gefolgt von der ON-Bedingung. Leider unterstützt nicht jedes Datenbanksystem den FULL OUTER Join direkt. Insbesondere MariaDB bietet standardmäßig keine native Implementierung für den FULL OUTER Join. Daher muss man in MariaDB ein wenig tricksen, um das gleiche Ergebnis zu erzielen. Man erreicht dies, indem man das Ergebnis eines LEFT Joins und das Ergebnis eines RIGHT Joins miteinander kombiniert. Dies geschieht mithilfe des UNION-Operators. Man führt also zwei separate Abfragen durch: eine, die einen LEFT Join zwischen den beiden Tabellen macht, und eine zweite, die einen RIGHT Join zwischen denselben Tabellen durchführt. Die Ergebnisse dieser beiden Abfragen werden dann mit UNION zusammengeführt. Der UNION-Operator kombiniert die Ergebnismengen und entfernt dabei automatisch doppelte Zeilen. Schauen wir uns ein Beispiel für die Simulation eines FULL OUTER Joins in MariaDB an. Wir haben zwei Tabellen: „hersteller“ mit einer ID und dem Zulieferer sowie „produkt“ mit einer ID, dem Produktnamen und einer Spalte „h-s-link“, die auf die Hersteller-ID verweist und somit eine Fremdschlüsselbeziehung darstellt. Wir möchten alle Zulieferer und alle Produkte im Ergebnis sehen. Wenn Produkte von bestimmten Zulieferern geliefert werden oder Zulieferer bestimmte Produkte anbieten, soll diese Verbindung im Ergebnis erkennbar sein. Das heißt, wir wollen sowohl Zulieferer, die keine Produkte im System haben, als auch Produkte, die keinem Zulieferer zugeordnet sind, sowie alle passenden Kombinationen sehen. Um das Ziel eines FULL OUTER Joins in MariaDB zu erreichen, wie im Beispiel beschrieben, führen wir zwei separate Abfragen durch und verknüpfen deren Ergebnisse mit UNION. Die erste Abfrage ist ein LEFT Join zwischen der „hersteller“-Tabelle (links) und der „produkt“-Tabelle (rechts) auf der Bedingung, dass die Hersteller-ID mit dem Fremdschlüssel im Produkt übereinstimmt. Diese Abfrage liefert alle Hersteller und die Produkte, die sie herstellen. Die zweite Abfrage ist ein RIGHT Join zwischen denselben Tabellen. Diese Abfrage liefert alle Produkte und die Hersteller, die ihnen zugeordnet sind. Indem wir die Ergebnisse dieser beiden Abfragen mit UNION kombinieren, erhalten wir ein Ergebnis, das alle Zeilen aus beiden Join-Ergebnissen enthält, wobei doppelte Zeilen entfernt werden. Dies simuliert effektiv das Verhalten eines FULL OUTER Joins: Alle Hersteller werden gelistet, auch wenn sie keine Produkte haben, und alle Produkte werden gelistet, auch wenn sie keinem Hersteller zugeordnet sind. Diese Kombination ergibt eine kombinierte Liste von Zulieferern und Produkten. Für die Hersteller, die Produkten zugeordnet sind, werden die entsprechenden Produktnamen in derselben Zeile angezeigt. Für Hersteller, die keine Produkte im System haben, wird der Produktname mit NULL angezeigt. Umgekehrt, für Produkte, die keinem Hersteller zugeordnet sind, wird der Zulieferer mit NULL angezeigt. Dieses Ergebnis enthält also alle Datensätze, die entweder in der linken Tabelle, der rechten Tabelle oder in beiden übereinstimmend vorhanden sind. Manchmal möchte man bei einem FULL OUTER Join nicht die übereinstimmenden Datensätze sehen, sondern nur diejenigen, die in einer Tabelle vorhanden sind, aber keine Übereinstimmung in der anderen Tabelle haben. Dies wird als FULL OUTER Join with exclusion bezeichnet. Das Ziel ist hierbei, nur die Datensätze zu erhalten, die spezifisch für die linke Tabelle oder spezifisch für die rechte Tabelle sind, aber nicht in beiden übereinstimmen. Die Syntax baut auf einem FULL OUTER Join auf, fügt aber eine WHERE-Klausel hinzu. Diese Klausel filtert die Ergebnisse so, dass nur Zeilen angezeigt werden, bei denen entweder die ID aus der linken Tabelle NULL ist oder der Fremdschlüssel aus der rechten Tabelle NULL ist. Auch einen FULL OUTER Join with exclusion muss man in MariaDB, da es keinen nativen FULL OUTER Join gibt, durch eine Kombination von LEFT und RIGHT Joins und zusätzlichen Filterbedingungen simulieren. Wir verwenden wieder die Tabellen „hersteller“ und „produkt“. Gesucht werden alle Zulieferer, die keine Produkte anbieten, sowie alle Produkte, die keinem Zulieferer zugeordnet werden können. Das Ergebnis soll also nur die „nicht übereinstimmenden“ Zeilen aus beiden Tabellen zeigen. Dies erreichen wir, indem wir einen LEFT Join von „hersteller“ nach „produkt“ machen und mit einer WHERE-Klausel diejenigen Zeilen filtern, bei denen der Fremdschlüssel im Produkt NULL ist. Dies liefert uns die Hersteller ohne Produkte. Zusätzlich führen wir einen RIGHT Join von „hersteller“ nach „produkt“ durch und filtern hier die Zeilen, bei denen die ID des Herstellers NULL ist. Dies liefert uns die Produkte ohne zugeordneten Hersteller. Die Ergebnisse dieser beiden Abfragen werden dann mit UNION zusammengeführt, um das finale Ergebnis des FULL OUTER Joins with exclusion in MariaDB zu erhalten. Das Ergebnis des FULL OUTER Join with exclusion Beispiels in MariaDB zeigt genau das, was wir gesucht haben: alle Zulieferer, die keine Produkte im System haben, und alle Produkte, die keinem Zulieferer zugeordnet sind. Basierend auf den Beispieldaten der Tabellen „hersteller“ und „produkt“ sehen wir im Ergebnis nur zwei Zulieferer: Stahl AG und Gähn & Söhne, beide mit NULL in der Produktspalte. Dies sind die Hersteller, die keine Produkte anbieten. Zusätzlich sehen wir das Produkt Hammer mit NULL in der Zuliefererspalte. Dies ist das Produkt, dem kein Hersteller zugeordnet ist. Die übereinstimmenden Paare von Herstellern und Produkten, die bei einem einfachen FULL OUTER Join erscheinen würden, sind hier im Ergebnis absichtlich ausgeschlossen. Als Nächstes betrachten wir den CROSS Join. Ein CROSS Join ist eine Art von Join, die sich deutlich von den bisher besprochenen Join-Typen unterscheidet. Wenn Sie einen CROSS Join zwischen zwei Tabellen durchführen und dabei keine WHERE-Klausel verwenden, erzeugt das Datenbanksystem das sogenannte kartesische Produkt aus den beiden beteiligten Tabellen. Das kartesische Produkt ist eine mathematische Operation. In Bezug auf Datenbanktabellen bedeutet es, dass jede einzelne Zeile aus der ersten Tabelle mit jeder einzelnen Zeile aus der zweiten Tabelle kombiniert wird. Die Größe des Resultsets, also der Ergebnistabelle eines solchen kartesischen Produkts, ist entsprechend sehr groß. Sie entspricht genau der Anzahl der Zeilen in der ersten Tabelle multipliziert mit der Anzahl der Zeilen in der zweiten Tabelle. Die Begriffe Kartesisches Produkt, Kreuzprodukt und Cross Join werden in diesem Zusammenhang oft als synonym verwendet. Die grundlegende Syntax für einen CROSS Join ist recht einfach: Man wählt die gewünschten Spalten aus und gibt die beiden Tabellen an, verbunden durch CROSS Join. Schauen wir uns ein Beispiel für einen CROSS Join an. Wir haben zwei einfache Tabellen: „foods“, die verschiedene Lebensmittel auflistet, und „company“, die verschiedene Firmen auflistet. Wenn wir nun einen CROSS Join zwischen „foods“ und „company“ durchführen, ohne eine einschränkende WHERE-Klausel, wird jede Zeile aus der „foods“-Tabelle mit jeder Zeile aus der „company“-Tabelle kombiniert. Die Abfrage könnte so formuliert sein, dass sie den Produktnamen und die Einheit aus der „foods“-Tabelle sowie den Firmennamen und die Stadt aus der „company“-Tabelle auswählt und diese über einen CROSS Join verknüpft. MariaDB unterstützt übrigens auch die ältere SQL-Syntax für ein kartesisches Produkt, bei der man die Tabellennamen einfach durch ein Komma trennt im FROM-Teil, also „FROM foods, company“ – das ist äquivalent zu „FROM foods CROSS Join company“. Diese Folie verdeutlicht das Konzept des CROSS Joins anhand unseres Beispiels mit den Tabellen „foods“ und „company“. Wie bereits erwähnt, kombiniert ein CROSS Join ohne WHERE-Klausel jede Zeile der ersten Tabelle mit jeder Zeile der zweiten Tabelle. Das Ergebnis ist, dass jedes Lebensmittel aus der „foods“-Tabelle mit jeder Firma aus der „company“-Tabelle gepaart wird. Das Beispielbild zeigt, wie sich die Zeilen potenziell vervielfachen. Wenn die „foods“-Tabelle sieben Zeilen hat und die „company“-Tabelle fünf Zeilen hat, wird das Ergebnis des CROSS Joins sieben mal fünf, also fünfunddreißig Zeilen enthalten. Dies ist ein Beispiel dafür, wie ein CROSS Join eine sehr große Ergebnismenge erzeugen kann, da er im Wesentlichen eine „Jeder mit jedem“-Kombination durchführt. Das Ergebnis eines CROSS Joins kann, wie gezeigt, sehr umfangreich sein, insbesondere wenn die beteiligten Tabellen viele Zeilen enthalten. In einem solchen Ergebnis sieht man, wie jedes Lebensmittel, beginnend mit „Chex Mix“, dann „Cheez-It“, „BN Biscuit“ und so weiter, mit jeder einzelnen Firma aus der „company“-Tabelle kombiniert wird: „Order All“ aus Boston, „Jack Hill Ltd“ aus London, „Akas Foods“ aus Delhi und so weiter. Dies wiederholt sich dann für jedes weitere Lebensmittel. Ein CROSS Join erzeugt schnell eine sehr große Ergebnismenge, das kartesische Produkt. Während es in manchen speziellen Anwendungsfällen nützlich sein kann, sollte man sich der potenziellen Größe des Resultsets bewusst sein und es mit Bedacht einsetzen. Ohne eine einschränkende WHERE-Klausel, die bestimmte Kombinationen filtert, ist das Ergebnis oft nicht direkt aussagekräftig für relationale Daten, sondern stellt eben alle möglichen Paarungen dar.